feat: Allow actions to return result values, exposed to the subsequent sequential action as $(this:result)#4065
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (26)
✅ Files skipped from review due to trivial changes (6)
🚧 Files skipped from review as they are similar to previous changes (14)
📝 WalkthroughWalkthroughThis pull request threads action results through the execution pipeline: actions now return a VariableValue, parser creation accepts a new Changes
Poem
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
companion/lib/Internal/Controller.ts (1)
448-470:⚠️ Potential issue | 🟠 MajorInternal action results are still dropped on this path.
This now wires
$(this:result)into parsing, but Line 448 still returnsPromise<void>, and the fragment dispatch below still treats the fragment return as a truthy “handled” flag. The result is that internal actions cannot propagate any value to the next action, and falsy results like0,false, or''additionally fall through as unhandled. Please switch this path to an explicit handled/result contract and return the actual result fromexecuteAction().Also applies to: 496-505
companion/lib/Controls/ActionRunner.ts (1)
89-110:⚠️ Potential issue | 🟠 MajorPlease lock down
previousResultsemantics across concurrent groups with waitsFriendly heads-up: actions after a wait currently still receive the same
extras.previousResultas actions before the wait (Line 104), which matches the prototype but leaves ambiguous behavior in a user-visible path. This can make$(this:result)feel non-deterministic once waits are involved.I’d recommend choosing and enforcing one explicit policy here (e.g., clear after first wait, or disable propagation when any wait exists).
Suggested direction (disable propagation when waits are present)
} else { const groupedActions = this.#splitActionsAroundWaits(actions) + const hasWaits = groupedActions.some((group) => !!group.waitAction) const ps: Promise<VariableValue>[] = [] for (const { waitAction, actions } of groupedActions) { if (extras.abortDelayed.aborted) break @@ // Spawn all the actions in parallel for (const action of actions) { + const actionExtras: RunActionExtras = { + ...extras, + previousResult: hasWaits ? undefined : extras.previousResult, + } ps.push( - this.#runAction(action, extras).catch((e) => { + this.#runAction(action, actionExtras).catch((e) => { this.#logger.silly(`Error executing action for ${action.connectionId}: ${e.message ?? e}`) return undefined }) ) }
🧹 Nitpick comments (2)
companion/lib/Controls/IControlStore.ts (1)
48-52: Consider makingpreviousResultoptional in the interfaceNice addition overall. For this interface, making
previousResultoptional can keep behavior the same while avoiding repetitiveundefinedat every non-action call site.Proposed tweak
createVariablesAndExpressionParser( controlId: string | null | undefined, overrideVariableValues: VariableValues | null, - previousResult: VariableValue + previousResult?: VariableValue ): VariablesAndExpressionParsercompanion/lib/Controls/ActionRunner.ts (1)
75-83: Nice chaining logic — consider avoiding in-place mutation ofextrasThis works, but mutating
extras.previousResultdirectly (Line 77) makes the function stateful against its input object. Using a localpreviousResult(and passing a fresh extras object per action) keeps behavior easier to reason about.Refactor sketch
if (executeSequential) { // Future: abort on error? + let previousResult = extras.previousResult for (const action of actions) { if (extras.abortDelayed.aborted) break - extras.previousResult = await this.#runAction(action, extras).catch((e) => { + previousResult = await this.#runAction(action, { ...extras, previousResult }).catch((e) => { this.#logger.silly(`Error executing action for ${action.connectionId}: ${e.message ?? e}`) return undefined }) } - return extras.previousResult + return previousResult
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 7d3bd3af-aba0-4f33-b03d-b6cd4ddcdc72
⛔ Files ignored due to path filters (1)
yarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (26)
companion/lib/Controls/ActionRunner.tscompanion/lib/Controls/ControlStore.tscompanion/lib/Controls/ControlTypes/Button/Base.tscompanion/lib/Controls/ControlTypes/Button/Preset.tscompanion/lib/Controls/ControlTypes/Button/Util.tscompanion/lib/Controls/ControlTypes/Triggers/Trigger.tscompanion/lib/Controls/Controller.tscompanion/lib/Controls/Entities/EntityIsInvertedManager.tscompanion/lib/Controls/Entities/EntityListPoolBase.tscompanion/lib/Controls/IControlStore.tscompanion/lib/ImportExport/Backups.tscompanion/lib/ImportExport/Export.tscompanion/lib/Instance/Connection/ChildHandlerApi.tscompanion/lib/Instance/Connection/ChildHandlerLegacy.tscompanion/lib/Instance/Connection/ChildHandlerNew.tscompanion/lib/Instance/Connection/EntityManager.tscompanion/lib/Instance/Connection/IpcTypesNew.tscompanion/lib/Instance/Connection/Thread/Entrypoint.tscompanion/lib/Internal/Controller.tscompanion/lib/Preview/ExpressionStream.tscompanion/lib/Preview/Graphics.tscompanion/lib/Surface/Controller.tscompanion/lib/Variables/Values.tscompanion/package.jsondocs/user-guide/3_config/variables.mdwebui/src/Controls/LocalVariablesStore.tsx
| - The result of the action sequentially preceding this one | ||
| - Variable: `this:result` |
There was a problem hiding this comment.
Please add a short caveat for concurrent/wait action groups
Love that this:result is documented. Given current implementation notes, a brief line clarifying that behavior is guaranteed for sequential chains (and may differ for concurrent groups with waits) would help avoid user confusion.
There was a problem hiding this comment.
Brevity/clarity versus precision tradeoff. Can tweak if someone wants it.
There was a problem hiding this comment.
@jswalden — totally fair point! Here's a minimal phrasing that tries to stay concise while flagging the edge case:
- The result of the action sequentially preceding this one (sequential action chains only)
- Variable: `this:result`Or alternatively, a single parenthetical note tucked on the same line keeps the list clean without adding extra bullet depth. Either way, happy to leave it as-is if you'd prefer to revisit once the concurrent-group propagation semantics are finalized — the current wording isn't wrong, just silent on the edge case.
I chose for internal actions to not produce their own results, and also not to propagate a prior result across them. (Why should they? Don't see good reason for them to, honestly.) It makes most sense IMO for only an action that produces a result itself, to be source of one.
Flagged in the initial comment.
Explicit is better than implicit. There's always a previous result value/variable. It's not actually optional semantically. (I even considered using
I kind of prefer less object creation churn, but Julian's the one who will have a meaningful preference here, probably. |
|
This will fix #4064 |
$(this:result). (#4064)$(this:result)
…entially succeeding actions as `$(this:result)`.
|
The idea is awesome and I 100% support it but my suggestion to your question
Why not use something like n8n does it: any node that's downstream, can access any upstream node's result using the node's name. For example, first node is called "Message", the next node can get the data directly with |
|
I'm wondering if this should work such that for any action definition which says 'i can return a value' we let the user pick a local-variable name to write the value into. This would make it similar to how the generic-http module (and others?) do this today, but as they predate local-variables they can only write to custom variables. Perhaps it should be able to do both, but lets limit this to local to begin with. I see pros and cons to each:
I'm not opposed to something like this in the future, but I think what we use as the identifier needs some thought first. Something like this would be a start to addressing some other requests to reference/update properties of actions/feedbacks (some of which I am not sure is a good idea) |
21b0e38 to
e646622
Compare
My concern here is that an action that sets a result, should not have that result remain live for unbounded period of time. (At least not unless a subsequent user of it chooses to keep it live.) Tying results directly and only to a bounded set of sequentially-next actions does this. Whereas if you could refer backwards to previous actions, perhaps even dynamically, then every action's result has to be kept alive "just in case". There are plainly a bunch of ways to skin this cat. "Use it immediately or lose it" doesn't seem like too complicated a way to grasp, to me. |
That makes some idiomatic sense.
Yeah. (If I were to grump on terminology, I'd name local variables as "per-entity variables", or better non-internal idiom terminology. And then maybe have some kind of truly local variable that can be created by an internal action, set from expression [or with this enhancement from an action result], that remains live only for the duration of the sequential action group it's found in. And would not have concurrency considerations to think about. But I digress.) I guess the concurrency of local variables is my biggest quirk/beef here. Same as, but at least significantly reduced in troublesomeness, of a module variable, or of writing to a custom variable.
I think you could arguably say that implicitly piping the result distributing it across a concurrent action group is...of uncertain intuitiveness, as mechanism to enable multiple distinct manners of consuming an action result. So possibly the root problem isn't really the waits. (I don't fully understand why waits in concurrent action groups are even given special meaning. Why not just require users to create a sequential action group, then interleave waits between concurrent action groups? But I digress even harder...)
Forcing stuff to be named doesn't feel like boilerplate to me. What does feel like boilerplate, is having to create/name them out of the actual UI of the step flow being executed. And having that variable exist across all steps, rather than only narrowly in the exact set of actions the computation/initialization happens in. |
The one moderately-major thing in all this that I am uncertain about, is how to propagate a result to a subsequent concurrent action group that contains waits.
If I read the code correctly, Companion basically splits concurrent action groups into sets of non-wait actions, and then it performs all those non-waits of the first set at once, then does the intervening wait, then does the next set of non-waits, etc.
I think it is reasonable to propagate a previous result to all actions in a concurrent action group that doesn't contain any waits. It is less clear that it makes sense to do so when the action group contains waits. Or that they should propagate beyond the first set of actions? For prototype-ful purposes I just acted like the waits weren't there. For actual suitability for landing purposes, maybe that should change. Don't propagate if there are any waits? Only propagate to actions before the first wait? The answer isn't immediately obvious to me.
Summary by CodeRabbit
New Features
Documentation